Skip to content

review: ground a cluster member on its shared anchor, not only the evidence's vocabulary - #363

Merged
jwbron merged 3 commits into
mainfrom
agent/pra42-cluster-grounding
Aug 21, 2026
Merged

review: ground a cluster member on its shared anchor, not only the evidence's vocabulary#363
jwbron merged 3 commits into
mainfrom
agent/pra42-cluster-grounding

Conversation

@jwbron

@jwbron jwbron commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Run 32390393344 (webapp#41609, review-v1.14.0 install, lib at 1.17.0) posted one finding twice: two comments in one run at 16:28:08 on moderation_helpers.go:31, one labeled thought and one question, both carrying the same claim (the experiment enrolled 3 configs, the change flips ~112). The author's reply was "broader rollout is intended and safe, stop raising this."

The artifact says the clusterer did its job: it proposed exactly one cluster with both ids. The merge died in the grounding tripwire, which requires the member's text to share a salient code token with the cluster's evidence. The evidence spoke in the hunk's identifiers (_configIncludesModeration, shouldModerateDuringMainCompletion) while both claims spoke config-side (pre_flight_moderation_check, config_files, 112/151), zero shared tokens, so first-principles-1 was rejected "ungrounded". The evidence is model prose with free word choice, so the check was grading the clusterer's phrasing rather than the identity it asserted.

Two changes:

  • An exactly shared anchor now grounds a proposed member with no vocabulary needed: the member sits on the survivor's own line (paths already match structurally). A cross-source pair on the identical line is the one identity assertion the module can verify without any words, and only claims the model proposed reach this check, so a same-line neighbour it never named can't ride in on its anchor.
  • Salient tokens fold casing styles (PreFlightModerationCheck and pre_flight_moderation_check read as one token), so the vocabulary path tests names, not spellings.

Considered and rejected: grounding the member against the survivor's own claim text. The suite already holds the counterexample; run 30587343777's cap survivor names staleAfter in a while-here aside, and the distinct staleAfter finding (documentation-2) would falsely ground against it. That rejection stays pinned.

Two existing tests changed meaning deliberately. The inert-evidence test now splits: the two members on the survivor's exact line merge on the anchor (the run's own autofix discharged all four asks with one rewritten comment, so that's the true outcome), while the line-9 member still needs the evidence and stays rejected. The no-subtraction test moves its thin-worded copy one line off so it keeps exercising the ungrounded path it exists to pin. The 41609 pair replays verbatim in a new dedup-cluster-grounding.test.ts (the old file hit the 1000-line cap, same split precedent as dedup.test.ts before it) and merges to one comment.

Follow-up, tracked separately: audit the 90-day window of dispatch-result artifacts for "ungrounded" rejections to get the before/after rate, and decide whether the vocabulary path earns its keep at all.

Review feedback folded in (1534d1d): merge records now say which path grounded each clusterer-absorbed copy (groundedBy: "anchor" | "evidence"), so that audit is answerable from the artifacts; a new test pins the anchor path's accepted cost (a distinct non-blocking finding on the survivor's exact line folds in when the model proposes it); and the clusterer prompt says "normally discarded" instead of promising the mechanical discard the code stopped making.

KORE-2509

…s shared anchor, not only the evidence's vocabulary

Run 32390393344 (webapp#41609) posted one finding twice: the clusterer
correctly proposed the same-line cross-source pair as one cluster and the
grounding tripwire vetoed it, because the evidence spoke in the hunk's
identifiers while both claims spoke config-side, zero shared salient
tokens. The evidence is model prose with free word choice, so the check
graded the clusterer's phrasing rather than the identity it asserted.

An exactly shared anchor (member on the survivor's own line) now grounds
a proposed member with no vocabulary needed, and salient tokens fold
casing styles so pre_flight_moderation_check and PreFlightModerationCheck
read as one token. Survivor-text grounding was considered and rejected;
run 30587343777's pinned fixtures show the false merge it would cause.
The 41609 pair replays verbatim as a regression fixture and merges to
one comment.
@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899519e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

Baseline: origin/main (review.md 4bc6293a03d1); candidate: working tree (review.md 0b94d4336236).

Ruler: matcher deterministic+arbiter; corpus 0441b7c6a581 (10 cases).

Metric Baseline Candidate Delta
Must-catch recall 100% 100% +0%
Verdict agreement 90% 90% +0%
Noise (unmatched posted) 69% 70% +1%
Clean false flags 1 1
Judge mean quality 0.87 0.89 +0.02
Cost $9.87 $9.85
Wall clock 1717s 1648s
Cases run / skipped 10 / 0 10 / 0
Misses found-but-dropped 0 0
Findings anchor-snapped 0 0
Cross-source claims merged (of candidates) 6 / 35 (2 by clusterer at $0.48 / 247s, 6 proposed member(s) rejected) 4 / 34 (0 by clusterer at $0.41 / 185s, 8 proposed member(s) rejected)

Adversarial hard gate: PASSED on the candidate arm.

Single-run-stable rows: recall, verdict agreement, regressions, adversarial gate. Judge quality and noise are not: they jitter run-to-run at this corpus size, and a regressed reviewer can score HIGHER on judge quality (fewer, surer comments each read better). Recall against the labeled specs is the load-bearing metric.

Measured noise floor (identical arms, run 29069228968, 2026-07-10, 6 arm-samples, full corpus x3, pre-arbiter; budget skips left the samples on unequal case sets, so these v1 bands also carry case-mix variance): must-catch recall 54%-86% (sd 10%), verdict agreement 75%-100% (sd 9%), noise (unmatched posted) 50%-60% (sd 3%), judge mean quality 82%-86% (sd 2%). A single-run delta whose arms both sit inside a band is indistinguishable from run-to-run wobble; use --repeats to resolve smaller effects.

@khan-actions-bot
khan-actions-bot requested review from a team, jaredly and somewhatabstract and removed request for a team August 20, 2026 18:33

@sxkosone sxkosone left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving these to keep review bot work ongoing, but haven't read the PR description or the code

@github-actions

This comment has been minimized.

@khan-actions-bot
khan-actions-bot requested a review from a team August 20, 2026 18:58

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review details review-v1.17.1 | schema 2 | depth full | re-review scoped blocking-only | enable holistic,completeness,test-adequacy,first-principles,conventions,documentation

Comment thread workflows/review/lib/dedup-cluster.ts
Comment thread workflows/review/lib/dedup.ts
Comment thread workflows/review/lib/dedup-cluster.test.ts
Comment thread workflows/review/lib/dedup.ts
Comment thread workflows/review/lib/dedup-cluster.ts
Comment thread workflows/review/lib/dedup-cluster.ts
Comment thread workflows/review/lib/dedup-cluster.ts
Comment thread workflows/review/lib/dedup-cluster.ts
if (structural !== undefined) {
return structural;
}
if (member.line !== undefined && member.line === survivor.line) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): The anchor path grounds on colocation, not identity — it retires the tripwire exactly where distinct-defect collisions are densest. isSalientToken's doc (dedup-cluster.ts:73) states the asymmetry this tier is calibrated on: "a missed merge costs a duplicate comment while a wrong one drops a reviewer's distinct finding" — yet same path + same line + proposed-by-the-model is now sufficient, and the offered safeguard ("only claims the model PROPOSED reach this check") is the very assertion the tripwire existed to second-guess; the sibling test "keeps the run's neighbours on the same lines out of the cluster" exists because distinct defects routinely share a line, and the pinned staleAfter counterexample only survives because it sits at :11 rather than :8. It's also worth asking about ordering: the PR defers the 90-day audit of "ungrounded" rejections to a follow-up, but that audit is the evidence that would say whether one measured false veto justifies retiring the check for same-line members, or whether a narrower fix (e.g. requiring the evidence to be groundable at all before the anchor shortcut applies) covers run 32390393344 without the blast radius.

review details found by first-principles

Comment thread workflows/review/lib/dedup-cluster-grounding.test.ts Outdated
…h's cost

Folds in the non-blocking review feedback on #363:

- each clusterer-absorbed copy in dispatch-result.json carries
  groundedBy: "anchor" | "evidence", so the planned audit of
  "ungrounded" rejections can tell the two paths apart
- a new test pins the anchor path's accepted cost: a distinct
  non-blocking finding on the survivor's exact line folds in when the
  model proposes it (documentation-2 re-anchored to :8)
- dedup.ts's determinism-boundary header no longer states the
  vocabulary check as unconditional
- the clusterer prompt says "normally discarded" instead of promising
  a mechanical discard the code stopped making
- sharesSalientToken is unexported (this PR removed its last importer)
- the empty-canonical guard gets a comment and a test (an
  all-underscore token folds to "" and would ground any two claims)
- the runClaims fixture moves to module scope, matching its siblings
@jwbron

jwbron commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Folded the review feedback in with 1534d1d (threaded replies are blocked for my token, so batching here):

  • groundedBy: merge records now carry groundedBy: "anchor" | "evidence" per clusterer-absorbed copy, so the 90-day audit of "ungrounded" rejections is answerable from dispatch-result artifacts instead of by re-deriving grounding from raw claims. That also bears on the ordering question in the anchor-path thread: shipping ahead of the audit costs less when the artifacts record which path each merge took.
  • The anchor path's accepted cost is pinned now: dedup-cluster-grounding.test.ts absorbs documentation-2 re-anchored to the survivor's :8, a genuinely distinct non-blocking finding folding in on the model's word (at its real :11 it still stays rejected). On the narrower fix (anchor shortcut only when the evidence is groundable at all): it would cover run 32390393344, whose evidence named real identifiers, but it would re-veto the same-line copies under inert evidence, and run 30587343777's autofix discharged all four of those asks with one rewritten comment, so that merge is the true outcome. The identity signal the anchor path trusts (exact line, cross source, model-proposed, non-blocking only) doesn't get stronger because the evidence happens to be well-worded.
  • The prompt stays strict on purpose: telling the clusterer that same-line members get a pass would train it to lean on the anchor and write lazier evidence, hurting exactly the members that aren't same-line. But it did promise a guarantee the code stopped making, so review.md now says "normally discarded" and drops "voids the group". The installed .github/workflows/review.md picks that up on the next version bump as usual.
  • Also in: the stale dedup.ts determinism-boundary sentence, sharesSalientToken unexported, a comment plus test for the empty-canonical guard (an all-underscore token folds to "" and would ground any two claims quoting a Go blank identifier), and the runClaims fixture moved to module scope.

Not taken: a merge-level test where the casing fold alone grounds a member; the fold stays pinned at the salientTokens level.

@github-actions

Copy link
Copy Markdown
Contributor

Guidance for reviewers

Triage notes for reviewers: risky files by owning team, repeated changes, and files excluded from review.

github-actions (3 files)
File Reason
dedup-cluster.ts Rewrites the merge gate in the shared reviewer's enforcement layer, so a mistake here silently folds away a distinct reviewer's comment on every consumer PR rather than failing loudly.
dedup.ts Hosts the tier-2 merge loop and the dispatch-result audit record, and this diff removes the group-level grounding gate so the bound now rests entirely on the per-member rule it delegates to.
review.md The shared reviewer prompt is the reviewer's behavior in every consuming repo; this edit only realigns the clusterer's description of the grounding check with the code, so the blast radius is wording accuracy.

Common patterns

3 files: Adding a groundedBy?: "anchor" | "evidence" discriminant field to merged/absorbed member objects: propagated through the ClaimMerge type definition, the absorbed-map element type, the runtime push site, and every test assertion that checks a clusterer-merged member.

-    {index: number; via?: "clusterer"}[]
+    {
+        index: number;
+        via?: "clusterer";
+        groundedBy?: "anchor" | "evidence";
+    }[]
-                        via: "clusterer",
+                        via: "clusterer",
+                        groundedBy: "evidence",
review details review-v1.17.1 | schema 2 | depth full | re-review scoped blocking-only | enable holistic,completeness,test-adequacy,first-principles,conventions,documentation

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 of 10 prior review threads resolved; 2 still unaddressed as of 1534d1d:

2 non-blocking threads still open
Note: 1 finding(s) not re-posted (already tracked in open review threads). Note: divergence tripwire re-armed a full review (unreviewed share 0.71).
review details review-v1.17.1 | schema 2 | depth full | re-review scoped blocking-only | enable holistic,completeness,test-adequacy,first-principles,conventions,documentation

into.push({
index,
via: "clusterer",
groundedBy:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): groundedBy re-derives the grounding decision instead of receiving it from clusterMemberRejection, so the two can silently disagree. The two expressions are identical today, so this is not a live bug — but the field's stated purpose (its JSDoc at dedup.ts:132-139, and the PR description's follow-up) is to feed an audit that decides whether the vocabulary tripwire earns its keep, and the only thing keeping the record honest is the // Mirrors the first grounding test in clusterMemberRejection comment two lines above. A duplicated predicate guarding a decision about deleting a safety check is worth collapsing to one source of truth: have the rejection helper return which path admitted the member, and let the caller record what the check actually decided rather than re-deciding it.

A sketch, not a committable replacement:

// in dedup-cluster.ts
export const clusterMemberAdmission = (
    survivor: Claim,
    member: Claim,
    evidenceTokens: ReadonlySet<string>,
):
    | {reason: ClusterRejection["reason"]}
    | {reason?: undefined; groundedBy: "anchor" | "evidence"} => {
    const structural = structuralRejection(survivor, member);
    if (structural !== undefined) {
        return {reason: structural};
    }
    if (member.line !== undefined && member.line === survivor.line) {
        return {groundedBy: "anchor"};
    }
    const evidenceUsable =
        evidenceTokens.size > 0 && sharesSalientToken(evidenceTokens, survivor);
    return evidenceUsable && sharesSalientToken(evidenceTokens, member)
        ? {groundedBy: "evidence"}
        : {reason: "ungrounded"};
};

// in dedup.ts, replacing the reason/push pair
const admission = clusterMemberAdmission(
    survivor,
    claims[index],
    evidenceTokens,
);
if (admission.reason !== undefined) {
    for (const id of namedByHead.get(index) ?? []) {
        clusterRejections.push({id, reason: admission.reason});
    }
    continue;
}
into.push({index, via: "clusterer", groundedBy: admission.groundedBy});
review details found by correctness-reviewer | also flagged by skill-auditor (out-of-lane); holistic; conventions (at line 856): Anchor rule re-derived inline in dedup.ts instead of being read from dedup-cluster.ts.

* must exist, the paths and sources must satisfy the same constraints tier 1
* enforces, the model's own grounding evidence must appear in every member's
* text, and only a NON-BLOCKING copy may be absorbed on a model's word
* text unless the member sits on the survivor's exact line, and only a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The change updates three statements of the grounding rule but leaves dedup.ts's canonical "the line does nothing in tier 2" paragraph contradicting it. The diff carefully amends the determinism-boundary paragraph (dedup.ts:91), structuralRejection's "deliberately absent" note, and clusterMemberRejection's docblock — but dedup.ts:68-73, the "unit of identity, restated" paragraph that is the module's most-quoted statement of why tier 2 exists, still asserts the line carries no weight in tier 2, which is now the opposite of what clusterMemberRejection does first. In a module whose headers are the design record and are cited by run number, the one paragraph a reader treats as canonical is the one left stale.

A sketch, not a committable replacement:

// dedup.ts, the "unit of identity, restated" paragraph (~:72):
 * pairs that also clear its looser text floor). The line is never REQUIRED in
 * tier 2; it counts only the other way round, as evidence — inside tier 1 as a
 * lower text floor, and in tier 2 as the one grounding a model's proposal needs
 * no vocabulary for (see {`@link` clusterMemberRejection}) — plus as the
 * survivor's posting anchor.
review details found by holistic

Comment thread workflows/review/lib/dedup-cluster.ts
Comment thread workflows/review/review.md
…sing fold, and the anchor against a tier-1 head

Two tests answering the open review threads on #363, no behavior change:

- The casing fold was exercised only as a salientTokens unit assertion, and
  the motivating run would not have needed it (its two spellings live in the
  two claims, and grounding compares the evidence against each claim). A new
  test pins a merge outcome on the fold: a cross-line member whose claim
  spells the evidence's config key in the other casing convention merges on
  the vocabulary path, and is rejected ungrounded without the fold.

- Nothing pinned the anchor path against a survivor the clusterer never
  named. A new test holds the transitivity chain: tier 1 bridges the named
  member into a blocking head on the same line, and the proposed member
  grounds on the anchor against that head even though the head never names
  the evidence (the survivor-end veto the vocabulary path would have made).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — no blocking issues found.
3 of 6 prior review threads resolved; 3 still unaddressed as of 899519e:

3 non-blocking threads still open (1 previously reported)
Non-blocking observations (5)
  • workflows/review/lib/dedup-cluster-grounding.test.ts:129 suggestion (non-blocking): The anchor grounding rule is decided in dedup-cluster.ts but re-derived in dedup.ts to label it. (holistic)
  • workflows/review/lib/dedup-cluster-grounding.test.ts:302 note (non-blocking): The anchor path's stated bound ("only claims the model PROPOSED reach this check") does not survive tier-1 head substitution at the member end. (holistic)
  • workflows/review/lib/dedup-cluster-grounding.test.ts:25 suggestion (non-blocking, documentation): Fixture provenance names a version the run did not use. (documentation)
  • workflows/review/lib/dedup-cluster-grounding.test.ts:298 question (non-blocking): When the survivor is a tier-1 head, the shared anchor is co-location with a claim the model never named, not with the one it did. (first-principles)
  • workflows/review/lib/dedup-cluster-grounding.test.ts:37 nitpick (non-blocking): Fixture factory is named for its shape, not the defect it carries, unlike the two files this one says it mirrors. (conventions)
Note: re-review ran at scoped depth (re-review mode scoped, blocking-only).
review details review-v1.17.1 | schema 2 | depth scoped | re-review scoped blocking-only | enable holistic,completeness,test-adequacy,first-principles,conventions,documentation

@jwbron
jwbron merged commit 7d9b6d6 into main Aug 21, 2026
10 checks passed
@jwbron
jwbron deleted the agent/pra42-cluster-grounding branch August 21, 2026 16:39
jwbron added a commit that referenced this pull request Aug 24, 2026
🖍 _This is an audit!_ 🖍

This repo's installed reviewer was on review-v1.17.1; 1.18.0 is cut and carries the prose judge (#362), whose rollout wants a week of live artifacts to watch the judge's error rate, so let's just move our own install first. Also in the hop: shared-anchor grounding with casing-folded salient tokens (#363), staged jira ticket context (#361, optional config we don't set here; staging degrades to available:false), and the turn-cap/timeout raises (#369).

Mechanics, per the hand-merge flow (not `gh aw update`): 3-way merge of the installed review.md from 1.17.1 to 1.18.0, one conflict where the new lib-checkout ref landed against the LOCAL OVERRIDE comment, resolved by keeping the override and taking the new ref; `source:` and the checkout `ref:` moved to review-v1.18.0 together; recompiled with gh-aw v0.85.4 (same compiler as the current lock); the compile's merge=ours strip on .gitattributes reverted. All 6 KHAN/ACTIONS LOCAL OVERRIDE blocks survived, and review-pins.test.ts is green locally (5 tests, including the content guard diffing the install against the pinned source).

[KORE-2512](https://khanacademy.atlassian.net/browse/KORE-2512)


[KORE-2512]: https://khanacademy.atlassian.net/browse/KORE-2512?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Author: jwbron

Auditors: jeresig, jaredly

Required Reviewers:

Approved By:

Checks: ⌛ 1 check is pending, ⏭️  8 checks have been skipped, ✅ 1 check was successful, ❌ 1 check failed

Pull Request URL: #373
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants